﻿using System;
using System.Diagnostics;
using System.IO;

namespace SfsTools.Core
{
    /// <summary>
    /// Implements an <see cref="IVolume"/> that supports transactions.
    /// And by transactions we actually mean batch updates.
    /// </summary>
    public sealed class TransactionalVolume : Object, IVolume, IDisposable
    {
        #region Properties

        /// <summary>
        /// Contains the underlying volume.
        /// </summary>
        private IVolume FVolume;
        /// <summary>
        /// Indicates whether its OK for <see cref="Dispose"/> to close the underlying <see cref="IVolume"/>.
        /// </summary>
        private bool FClose;
        /// <summary>
        /// Contains a list of updates.
        /// </summary>
        private readonly System.Collections.Generic.List<Change> FUpdates = new System.Collections.Generic.List<Change>();

        /// <summary>
        /// Gets the count of uncommitted updates.
        /// </summary>
        public Int32 UpdateCount
        {
            [DebuggerStepThrough]
            get
            {
                return FUpdates.Count;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Commits all changes.
        /// </summary>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        public void Commit()
        {
            int index = 0;
            try
            {
                for (; index < FUpdates.Count; index++)
                {
                    FUpdates[index].Commit();
                }
                FUpdates.Clear();
            }
            catch (IOException)
            {
                // Attempt to roll back changes.
                for (; index >= 0; index--)
                {
                    FUpdates[index].Discard();
                }
                throw;
            }
        }

        /// <summary>
        /// Commits all changes.
        /// </summary>
        /// <param name="close">Controls whether a <see cref="IDisposable.Dispose"/> call can close this volume.</param>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        public void Commit(bool close)
        {
            int index = 0;
            try
            {
                for (; index < FUpdates.Count; index++)
                {
                    FUpdates[index].Commit();
                }
                FUpdates.Clear();
                if (close)
                    FClose = true;
            }
            catch (IOException)
            {
                // Attempt to roll back changes.
                for (; index >= 0; index--)
                {
                    FUpdates[index].Discard();
                }
                throw;
            }
        }

        /// <summary>
        /// Discards all changes.
        /// </summary>
        [DebuggerStepThrough]
        public void Discard()
        {
            FUpdates.Clear();
        }

        /// <summary>
        /// Discards all changes.
        /// </summary>
        /// <param name="close">Controls whether a <see cref="IDisposable.Dispose"/> call can close this volume.</param>
        [DebuggerStepThrough]
        public void Discard(bool close)
        {
            FUpdates.Clear();
            if (close)
                FClose = true;
        }

        /// <summary>
        /// Reads from the underlying volume.
        /// </summary>
        /// <param name="offset">The offset to read at.</param>
        /// <param name="count">The count of bytes to read.</param>
        /// <returns></returns>
        [DebuggerStepThrough]
        private byte[] Read(Int64 offset, int count)
        {
            byte[] result = new byte[count];
            BlockRegion region = new BlockRegion(offset, count);
            for (int index = FUpdates.Count - 1; index >= 0; index--)
            {
                BlockRegion updated = FUpdates[index].GetRegion();
                // u -> updated bytes
                // r -> requested bytes
                // R -> requested and updated
                // Check if [uuuuu[RRRRR]uuuuu]
                if (updated.Contains(region))
                {
                    // Fully contained.
                    byte[] before = FUpdates[index].GetBefore();
                    int delta = (int)(region.Start - updated.Start);
                    Array.Copy(before, delta, result, 0, count);
                    return result;
                }
                // Check if at least partially overlaps.
                if (updated.IsOverlapping(region))
                {
                    // Check if [uuuuuu[RRRR]rrrr]
                    if ((updated.Start < region.Start) && ((updated.Start + updated.Length) > region.Start))
                    {
                        // Read the updated portion.
                        byte[] before = FUpdates[index].GetBefore();
                        int delta = (int)(region.Start - updated.Start);
                        int length = (int)(updated.Length - delta);
                        Debug.Assert(length > 0);
                        Array.Copy(before, delta, result, 0, length);
                        // Read the untouched portion.
                        offset = offset - delta + updated.Length;
                        count = count - length;
                        byte[] splitRead = Read(offset, count);
                        Array.Copy(splitRead, 0, result, length, count);
                        return result;
                    }
                    // Check if [rrrrrr[RRRR]uuuu]
                    if ((updated.Start < (region.Start + region.Length)) && (updated.Length > region.Length))
                    {
                        // Read the untouched portion.
                        int delta = (int)(updated.Start - region.Start);
                        int length = (int)(region.Length - delta);
                        Debug.Assert(length > 0);
                        byte[] splitRead = Read(offset, length);
                        Array.Copy(splitRead, 0, result, 0, length);
                        // Read the updated portion.
                        count = count - length;
                        byte[] before = FUpdates[index].GetBefore();
                        Array.Copy(before, 0, result, delta, count);
                        return result;
                    }
                    // Check if [rrrrrr[RRRRR]rrrrrr]
                    if ((updated.Start > region.Start) && ((updated.Start + updated.Length) < (region.Start + region.Length)))
                    {
                        // Read the lower untouched portion.
                        int delta1 = 0;
                        int length1 = (int)(updated.Start - region.Start);
                        Debug.Assert(length1 > 0);
                        byte[] splitRead1 = Read(offset + delta1, length1);
                        Array.Copy(splitRead1, 0, result, delta1, length1);
                        // Read the upper untouched portion.
                        int delta2 = (int)(updated.Start + updated.Length - region.Start);
                        int length2 = (int)(region.Start + region.Length - (updated.Start + updated.Length));
                        Debug.Assert(length2 > 0);
                        byte[] splitRead2 = Read(offset + delta2, length2);
                        Array.Copy(splitRead2, 0, result, delta2, length2);
                        // Read the updated portion.
                        byte[] before = FUpdates[index].GetBefore();
                        Array.Copy(before, 0, result, length1, (int)region.Length);
                        return result;
                    }
                    // Impossible.
                    Debug.Fail("wii fail");
                }
            }
            // No uncommitted changes in this region.
            return FVolume.Read(offset, count);
        }

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="TransactionalVolume"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="TransactionalVolume"/>.
        /// </summary>
        /// <param name="volume">The underlying volume.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="volume"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public TransactionalVolume(IVolume volume)
            : base()
        {
            if (volume == null)
                throw new ArgumentNullException("volume");
            FVolume = volume;
        }
        #endregion
        
        #region IVolume Members

        /// <summary>
        /// Gets the length of the volume, in bytes.
        /// </summary>
        /// <exception cref="NotSupportedException">An attempt was made to query the size of the volume on a volume which does not support this operation.</exception>
        Int64 IVolume.Length
        {
            [DebuggerStepThrough]
            get
            {
                if (FVolume == null)
                    throw new ObjectDisposedException(null);
                else
                    return FVolume.Length;
            }
        }

        /// <summary>
        /// Reads <paramref name="count"/> bytes from the volume.
        /// </summary>
        /// <param name="offset">The byte offset to start reading from.</param>
        /// <param name="count">The count of bytes read.</param>
        /// <returns>An array contaning the bytes read.</returns>
        /// <exception cref="EndOfStreamException">There is not enough space to read all the bytes (more bytes requested than the volume contains).</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="count"/> is out of valid range.</exception>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        [DebuggerStepThrough]
        byte[] IVolume.Read(Int64 offset, Int32 count)
        {
            if (FVolume == null)
                throw new ObjectDisposedException(null);
            return Read(offset, count);
        }

        /// <summary>
        /// Writes the contents of <paramref name="buffer"/> to the volume.
        /// </summary>
        /// <param name="offset">The byte offset to start writing at.</param>
        /// <param name="buffer">The array containing the bytes to write.</param>
        /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        [DebuggerStepThrough]
        void IVolume.Write(Int64 offset, byte[] buffer)
        {
            if (FVolume == null)
                throw new ObjectDisposedException(null);
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            // Save 'before' state.
            byte[] before = Read(offset, buffer.Length);
            byte[] after = new byte[buffer.Length];
            Array.Copy(buffer, 0, after, 0, buffer.Length);
            Change change = new Change(this, offset, before, after);
            FUpdates.Add(change);
        }

        /// <summary>
        /// Writes the contents of <paramref name="buffer"/> to the volume.
        /// </summary>
        /// <param name="offset">The byte offset to start writing at.</param>
        /// <param name="buffer">The array containing the bytes to write.</param>
        /// <param name="index">The index into <paramref name="buffer"/> to copy the bytes from.</param>
        /// <param name="count">The count of bytes to copy.</param>
        /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        [DebuggerStepThrough]
        void IVolume.Write(Int64 offset, byte[] buffer, int index, int count)
        {
            if (FVolume == null)
                throw new ObjectDisposedException(null);
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if ((index < 0) || (index > buffer.Length))
                throw new ArgumentOutOfRangeException("index");
            if ((count < 0) || ((index + count) > buffer.Length))
                throw new ArgumentOutOfRangeException("count");
            // Save 'before' state.
            byte[] before = Read(offset, count);
            byte[] after = new byte[count];
            Array.Copy(buffer, index, after, 0, count);
            // (bug) we saved the user's buffer, not the after state.
            // And since buffer.Length > count was in the case, this triggered this latent bugg.
            Change change = new Change(this, offset, before, after);
            FUpdates.Add(change);
        }

        #endregion
        
        #region IDisposable Members

        /// <summary>
        /// Releases all resources used by this <see cref="TransactionalVolume"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        public void Dispose()
        {
            if (FClose)
            {
                if (FVolume != null)
                {
                    FVolume.Dispose();
                    FVolume = null;
                }
            }
        }

        /// <summary>
        /// Releases all resources used by this <see cref="TransactionalVolume"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        void IDisposable.Dispose()
        {
            if (FClose)
            {
                if (FVolume != null)
                {
                    FVolume.Dispose();
                    FVolume = null;
                }
            }
        }

        #endregion

        #region Helper classes

        /// <summary>
        /// Represents a change.
        /// </summary>
        private class Change
        {
            #region Properties

            /// <summary>
            /// Contains the parent instance.
            /// </summary>
            private readonly TransactionalVolume FParent;
            /// <summary>
            /// Contains the offset to update the volume at.
            /// </summary>
            private readonly Int64 FOffset;
            /// <summary>
            /// Contains the <em>before</em> state.
            /// </summary>
            private readonly byte[] FBefore;
            /// <summary>
            /// Contains the <em>after</em> state.
            /// </summary>
            private readonly byte[] FAfter;

            #endregion

            #region Methods

            /// <summary>
            /// Gets the region that is updated.
            /// </summary>
            /// <returns></returns>
            [DebuggerStepThrough]
            public BlockRegion GetRegion()
            {
                return new BlockRegion(FOffset, FAfter.Length);
            }

            /// <summary>
            /// Gets the <em>before</em> state.
            /// </summary>
            /// <returns></returns>
            [DebuggerStepThrough]
            public byte[] GetBefore()
            {
                return FBefore;
            }

            /// <summary>
            /// Commits the changes.
            /// </summary>
            /// <exception cref="IOException">An I/O error occurred.</exception>
            [DebuggerStepThrough]
            public void Commit()
            {
                FParent.FVolume.Write(FOffset, FAfter);
            }

            /// <summary>
            /// Rolls back the changes.
            /// </summary>
            /// <exception cref="IOException">An I/O error occurred.</exception>
            [DebuggerStepThrough]
            public void Discard()
            {
                FParent.FVolume.Write(FOffset, FBefore);
            }

            /// <summary>
            /// Converts this <see cref="Change"/> instance to a <see cref="String"/>.
            /// </summary>
            /// <returns></returns>
            [DebuggerStepThrough]
            public override String ToString()
            {
                return new System.Text.StringBuilder().
                    Append('{').Append(FOffset.ToString("X").PadLeft(8, '0')).Append('/').Append(FAfter.Length).Append('}').
                    ToString();
            }

            #endregion

            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="Change"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="Change"/>.
            /// </summary>
            /// <param name="parent">The parent instance.</param>
            /// <param name="offset">The offset to update the underlying <see cref="IVolume"/> at.</param>
            /// <param name="before">The <em>before</em> state.</param>
            /// <param name="after">The <em>after</em> state.</param>
            [DebuggerStepThrough]
            public Change(TransactionalVolume parent, Int64 offset, byte[] before, byte[] after)
                : base()
            {
                Debug.Assert(before.Length == after.Length);
                FParent = parent;
                FOffset = offset;
                FBefore = before;
                FAfter = after;
            }
            #endregion
        }

        #endregion
    }
}
